QuteBrowser 的命令模式架构实现
介绍
QuteBrowser、Vim、Emacs 的共同核心都是运行时的命令模式,在 QuteBrowser 中是如何实现的呢?在本文中进行梳理。
cmdutils
cmdutils 是一个用于方便命令注册的工具模块,位于 qutebrowser\api\cmdutils.py。
从源码注释中可以得知:
QuteBrowser 中有 Function 的概念,向用户暴露交互式的指令。
通过以下代码可以很容易创建一条指令:
from qutebrowser.api import cmdutils
@cmdutils.register(...)
def foo():
...
命令参数能通过检查函数自动推导出来。
函数参数的类型是根据参数默认值进行推导的,比如 foo=True
会转换为 -f
或者 --foo
,展示在 QuteBrowser 的命令行中。
也能够通过类型声明指定参数类型:def foo(bar: int, baz=True)
支持的类型:
- 一个可调用对象(``int', ``float', etc.)。被调用以验证/转换值。
- Python 枚举
- Python Union 多类型,比如
Union[str, int]
register 类
该类负责将命令注册到命令注册表中。
__call__ 方法
该方法执行实际注册操作:
def __call__(self, func: _CmdHandlerType) -> _CmdHandlerType:
"""Register the command before running the function.
Args:
func: The function to be decorated.
Return:
The original function (unmodified).
"""
if self._name is None:
name = func.__name__.lower().replace('_', '-')
else:
assert isinstance(self._name, str), self._name
name = self._name
cmd = command.Command(
name=name,
instance=self._instance,
handler=func,
**self._kwargs,
)
cmd.register()
# ……
return func
其中:
- 参数中的 func 是被装饰器注解的函数
- 这个方法只是将 func 拿过来,封装成一个 Command 实例,然后调用 Command.register()(内部工作肯定是向注册表中注册)
- 最后将该方法原原本本地返回出去
- 同时还有一个对函数名规范化的过程,将均转为小写,并将下划线转为横杠
Command 类
这是 QuteBrowser 命令所对应的实体。
属性
包含以下属性:
属性 | 类型 | 说明 |
---|---|---|
name | str | 命令名称 |
maxsplit | int | 对命令行进行分割的最大数量,或 None |
deprecated | str | 描述为何废弃该命令 |
desc | str | 命令描述 |
handler | 函数 | 命令对应的函数对象 |
debug | bool | 是否是调试命令,只在 --debug 时展示 |
parser | 用于解析参数的 ArgumentParser | |
flags_with_args | 一个带有参数的标志的列表。 | |
no_cmd_split | bool | 如果为真,用于分割子命令的 ';;' 将被忽略。 |
backend | 该命令适用于哪个后端(如果适用于两个后端则为无)。两者都适用) | |
no_replace_variables | 不要替换像 {url} 这样的变量。 | |
modes | 该命令运行在的模式 | |
_qute_args | 从 @cmdutils. 参数中保存的数据 | |
_count | 该命令的计数设置。 | |
_instance | 绑定 "self" 的对象。 | |
_scope | 要在对象注册表中获取 _instance 的范围。 |
构造函数
这里以上一节中,实际调用代码为例:
cmd = command.Command(
name=name,
instance=self._instance,
handler=func,
**self._kwargs,
)
cmd.register()
其中,最后一个参数 kwargs 需要关注,举几个实际调用的例子:
maxiee kargs = {'modes': [<KeyMode.command: 3>, <KeyMode.prompt: 5>]}
maxiee kargs = {'modes': [<KeyMode.command: 3>, <KeyMode.prompt: 5>], 'deprecated': "Use :rl-rubout ' ' instead."}
maxiee kargs = {'debug': True, 'maxsplit': 0, 'no_cmd_split': True}
maxiee kargs = {'modes': [<KeyMode.caret: 8>]}
maxiee kargs = {'scope': 'window'}
所以完整的构造参数,需要结合 kargs 一起来看。
构造函数里的部分核心逻辑:
- 如果 mode 没有执行,采用默认值
usertypes.KeyMode
:
- 其它的基本上都是保存到属性中
register 方法
实现如下:
def register(self):
"""Register this command in objects.commands."""
log.commands.vdebug( # type: ignore[attr-defined]
"Registering command {} (from {}:{})".format(
self.name, self.handler.__module__, self.handler.__qualname__))
if self.name in objects.commands:
raise ValueError("{} is already registered!".format(self.name))
objects.commands[self.name] = self
可以看到,把 Command 实例往 objects 中一存完事。 关于 objects 导入方式为:
from qutebrowser.misc import objects
该模块是一个保存全局单例用的,objects 内 commands 对应代码如下:
commands: Dict[str, 'command.Command'] = {}